Valdage Pythoni asyncio madala taseme võrgundust. See põhjalik sukeldumine hõlmab transporte ja protokolle, praktiliste näidetega suure jõudlusega, kohandatud võrgurakenduste ehitamiseks.
Pythoni Asyncio Transpordi demüstifitseerimine: põhjalik sukeldumine madala taseme võrgundusse
Kaasaegse Pythoni maailmas on asyncio
'st saanud suure jõudlusega võrgundusprogrammeerimise nurgakivi. Arendajad alustavad sageli selle kaunitest kõrgetasemeliste API-dega, kasutades async
ja await
teeke nagu aiohttp
või FastAPI
, et ehitada reageerivaid rakendusi märkimisväärse kergusega. StreamReader
ja StreamWriter
objektid, mis on saadud funktsioonidest nagu asyncio.open_connection()
, pakuvad imeliselt lihtsat, järjestikust viisi võrgu I/O käsitlemiseks. Aga mis juhtub siis, kui abstraktsioonist ei piisa? Mis siis, kui teil on vaja rakendada keerukat, olekupõhist või mittestandardset võrguprotokolli? Mis siis, kui peate pigistama igat viimast jõudluse piiska, juhtides otse aluseks olevat ühendust? Siin peitub asyncio võrgundusvõimaluste tõeline alus: madala taseme Transport ja Protokolli API. Kuigi see võib esialgu hirmutav tunduda, avab selle võimsa duo mõistmine uue kontrolli- ja paindlikkuse taseme, võimaldades teil ehitada peaaegu iga ette kujutatav võrgurakendus. See põhjalik juhend avab abstraktsioonikihte, uurib Transportide ja Protokollide sümbiootilist suhet ning viib teid läbi praktiliste näidete, et anda teile võimalus valdada madala taseme asünkroonset võrgundust Pythonis.
Asyncio võrgunduse kaks nägu: kõrge tase vs madal tase
Enne kui süveneme madala taseme API-desse, on oluline mõista nende kohta asyncio ökosüsteemis. Asyncio pakub arukalt kaks eraldi kihti võrgukommunikatsiooniks, millest igaüks on kohandatud erinevate kasutusjuhtumite jaoks.
Kõrge taseme API: vood
Kõrgetasemeline API, mida tavaliselt nimetatakse „voodeks“, on see, millega enamik arendajaid kõigepealt kokku puutub. Kui kasutate asyncio.open_connection()
või asyncio.start_server()
, saate StreamReader
ja StreamWriter
objekte. See API on loodud lihtsuse ja kasutusmugavuse jaoks.
- Käskiv stiil: See võimaldab teil kirjutada koodi, mis näeb välja järjestikune. Te
await reader.read(100)
, et saada 100 baiti, seejärelwriter.write(data)
, et saata vastus. Seeasync/await
muster on intuitiivne ja kergesti mõistetav. - Mugavad abimehed: See pakub meetodeid nagu
readuntil(separator)
jareadexactly(n)
, mis käsitlevad levinud raamistamisülesandeid, säästes teid puhvrite käsitsi haldamisest. - Ideaalne kasutusjuhtum: Sobib suurepäraselt lihtsate päringu-vastuse protokollide (nagu põhiline HTTP-klient), reaalipõhiste protokollide (nagu Redis või SMTP) või mis tahes olukorra jaoks, kus side järgib ettearvatavat, lineaarset voogu.
Kuid see lihtsus toob kaasa kompromissi. Voolupõhine lähenemine võib olla vähem tõhus väga samaaegsete, sündmustepõhiste protokollide puhul, kus soovimata sõnumid võivad saabuda igal ajal. Järjestikune await
mudel võib muuta samaaegsete lugemiste ja kirjutiste käsitlemise või keerukate ühendusolekute haldamise tülikaks.
Madala taseme API: transpordid ja protokollid
See on põhiline kiht, millele kõrgetasemeline Vood API tegelikult üles ehitatakse. Madala taseme API kasutab disainimustrit, mis põhineb kahel eristataval komponendil: transportidel ja protokollidel.
- Sündmustepõhine stiil: Selle asemel, et kutsuda funktsiooni andmete hankimiseks, kutsub asyncio teie objektil meetodeid, kui sündmused juhtuvad (nt ühendus luuakse, andmed võetakse vastu). See on tagasikutsumisel põhinev lähenemine.
- Murede lahutamine: See eraldab puhtalt „mida“ „kuidas“. Protokoll määratleb mida andmetega teha (teie rakenduse loogika), samas kui Transport käsitleb seda, kuidas andmeid võrgu kaudu saadetakse ja vastu võetakse (I/O mehhanism).
- Maksimaalne kontroll: See API annab teile peene kontrolli puhvrite, voolu reguleerimise (vastusrõhk) ja ühenduse elutsükli üle.
- Ideaalne kasutusjuhtum: Oluline kohandatud binaar- või tekstiprotokollide rakendamisel, suure jõudlusega serverite ehitamisel, mis käsitlevad tuhandeid püsivaid ühendusi, või võrguraamistike ja teekide arendamisel.
Mõelge sellele nii: Vood API on nagu toidukomplekti teenuse tellimine. Saate eelnevalt portsjoniteks mõeldud koostisosad ja lihtsa retsepti, mida järgida. Transport ja Protokolli API on nagu kokk professionaalses köögis toorainega ja täieliku kontrolliga iga etapi üle. Mõlemad võivad valmistada suurepärase söögikorra, kuid viimane pakub piiritut loovust ja kontrolli.
Põhikomponendid: lähemalt transportidest ja protokollidest
Madala taseme API jõud tuleb Protokolli ja Transpordi elegantse interaktsiooni kaudu. Need on eristatavad, kuid lahutamatud partnerid igas madala taseme asyncio võrgurakenduses.
Protokoll: teie rakenduse aju
Protokoll on klass, mille teie kirjutate. See pärib klassist asyncio.Protocol
(või mõni selle variant) ja sisaldab olekut ja loogikat üksiku võrguühenduse käsitlemiseks. Te ei loo seda klassi ise; esitate selle asyncio'le (nt loop.create_server
'ile) ja asyncio loob teie protokollist uue eksemplari iga uue kliendiühenduse jaoks.
Teie protokolli klass on määratletud sündmuste käsitlejate komplektiga, mida sündmuse tsükkel kutsub välja ühenduse elutsükli erinevatel punktidel. Kõige olulisemad neist on:
connection_made(self, transport)
Välja kutsutud täpselt üks kord, kui uus ühendus on edukalt loodud. See on teie sisenemispunkt. Siin saate transport
objekti, mis tähistab ühendust. Peaksite alati salvestama viite sellele, tavaliselt kui self.transport
. See on ideaalne koht ühenduspõhise lähtestamise tegemiseks, näiteks puhvrite seadistamine või võrdlusandmete logimine.
data_received(self, data)
Teie protokolli süda. Seda meetodit kutsutakse iga kord, kui ühenduse teisest otsast võetakse vastu uusi andmeid. data
argument on bytes
objekt. On ülimalt oluline meeles pidada, et TCP on vooluprotokoll, mitte sõnumiprotokoll. Üks loogiline sõnum teie rakendusest võidakse jagada mitmeks data_received
kõneks või mitu väikest sõnumit võidakse komplekteerida üheks kõneks. Teie kood peab seda puhverdamist ja parsimist käsitlema.
connection_lost(self, exc)
Välja kutsutud, kui ühendus katkestatakse. See võib juhtuda mitmel põhjusel. Kui ühendus katkestatakse puhtalt (nt teine pool sulgeb selle või helistate transport.close()
), on exc
väärtus None
. Kui ühendus katkestatakse vea tõttu (nt võrgu rike, lähtestamine), on exc
erandi objekt, mis kirjeldab viga. See on teie võimalus teha puhastus, logida lahtiühendamine või proovida uuesti ühendust luua, kui loote klienti.
eof_received(self)
See on peenem tagasihelistamine. Seda kutsutakse välja, kui teine ots annab märku, et ta ei saada enam andmeid (nt helistades shutdown(SHUT_WR)
POSIX-süsteemis), kuid ühendus võib ikkagi olla avatud, et saaksite andmeid saata. Kui tagastate sellest meetodist väärtuse True
, sulgub transport. Kui tagastate väärtuse False
(vaikimisi), vastutate transpordi ise hiljem sulgemise eest.
Transport: sidekanal
Transport on objekt, mille annab asyncio. Te ei loo seda; saate selle oma protokolli connection_made
meetodis. See toimib kõrgetasemelise abstraktsioonina aluseks oleva võrgusokli ja sündmuse tsükli I/O ajastuse üle. Selle peamine ülesanne on andmete saatmise ja ühenduse juhtimise käsitlemine.
Sa suhtlete transpordiga selle meetodite kaudu:
transport.write(data)
Peamine meetod andmete saatmiseks. data
peab olema bytes
objekt. See meetod ei blokeeri. See ei saada andmeid kohe. Selle asemel asetab see andmed sisesesse kirjutuspuhvrisse ja sündmuse tsükkel saadab need võrgu kaudu võimalikult tõhusalt taustal.
transport.writelines(list_of_data)
Tõhusam viis bytes
objektide jada korraga puhvrisse kirjutamiseks, mis võib vähendada süsteemikõnede arvu.
transport.close()
See algatab sujuva sulgemise. Transport tühjendab esmalt kõik kirjutuspuhvris järelejäänud andmed ja seejärel katkestab ühenduse. Pärast close()
kutsumist ei saa enam andmeid kirjutada.
transport.abort()
See viib läbi raske sulgemise. Ühendus katkestatakse kohe ja kõik kirjutuspuhvris ootel olevad andmed visatakse ära. Seda tuleks kasutada erandlikel asjaoludel.
transport.get_extra_info(name, default=None)
Väga kasulik meetod enesevaatluseks. Saate teavet ühenduse kohta, näiteks vastaspoole aadressi ('peername'
), aluseks olevat sokliobjekti ('socket'
) või SSL/TLS sertifikaadi teavet ('ssl_object'
).
Sümbiootiline suhe
Selle disaini ilu on teabe selges, tsüklilises voolus:
- Seadistamine: Sündmuste tsükkel aktsepteerib uut ühendust.
- Instantsimine: Tsükkel loob teie
Protocol
klassi eksemplari jaTransport
objekti, mis esindab ühendust. - Linkimine: Tsükkel kutsub välja
your_protocol.connection_made(transport)
, ühendades kaks objekti. Teie protokollil on nüüd võimalus andmeid saata. - Andmete vastuvõtmine: Kui andmed saabuvad võrgusoklile, ärkab sündmuste tsükkel, loeb andmed ja kutsub välja
your_protocol.data_received(data)
. - Töötlemine: Teie protokolli loogika töötleb saadud andmeid.
- Andmete saatmine: Oma loogika põhjal kutsub teie protokoll välja
self.transport.write(response_data)
, et saata vastus. Andmed puhverdatakse. - Taust I/O: Sündmuste tsükkel käsitleb puhverdatud andmete mitteblokeerivat saatmist transpordi kaudu.
- Lahtivõtmine: Kui ühendus lõpeb, kutsub sündmuste tsükkel lõpliku puhastuse jaoks välja
your_protocol.connection_lost(exc)
.
Praktilise näite koostamine: kaja server ja klient
Teooria on suurepärane, kuid parim viis transportide ja protokollide mõistmiseks on midagi üles ehitada. Loome klassikalise kaja serveri ja vastava kliendi. Server aktsepteerib ühendusi ja saadab lihtsalt tagasi kõik andmed, mida ta saab.
Kaja serveri rakendamine
Esmalt määratleme oma serveripoolse protokolli. See on märkimisväärselt lihtne, mis näitab põhilisi sündmuste käsitlejaid.
import asyncio
class EchoServerProtocol(asyncio.Protocol):
def connection_made(self, transport):
# A new connection is established.
# Get the remote address for logging.
peername = transport.get_extra_info('peername')
print(f"Connection from: {peername}")
# Store the transport for later use.
self.transport = transport
def data_received(self, data):
# Data is received from the client.
message = data.decode()
print(f"Data received: {message.strip()}")
# Echo the data back to the client.
print(f"Echoing back: {message.strip()}")
self.transport.write(data)
def connection_lost(self, exc):
# The connection has been closed.
print("Connection closed.")
# The transport is automatically closed, no need to call self.transport.close() here.
async def main_server():
# Get a reference to the event loop as we plan to run the server indefinitely.
loop = asyncio.get_running_loop()
host = '127.0.0.1'
port = 8888
# The `create_server` coroutine creates and starts the server.
# The first argument is the protocol_factory, a callable that returns a new protocol instance.
# In our case, simply passing the class `EchoServerProtocol` works.
server = await loop.create_server(
lambda: EchoServerProtocol(),
host,
port)
addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets)
print(f'Serving on {addrs}')
# The server runs in the background. To keep the main coroutine alive,
# we can await something that never completes, like a new Future.
# For this example, we'll just run it "forever".
async with server:
await server.serve_forever()
if __name__ == "__main__":
try:
# To run the server:
asyncio.run(main_server())
except KeyboardInterrupt:
print("Server shut down.")
Selles serveri koodis on loop.create_server()
põhiline. See seob määratud hosti ja pordiga ning ütleb sündmuste tsüklile, et see hakkaks uusi ühendusi kuulama. Iga sissetuleva ühenduse puhul kutsub see välja meie protocol_factory
(funktsioon lambda: EchoServerProtocol()
), et luua uus protokolli eksemplar, mis on pühendatud sellele konkreetsele kliendile.
Kaja kliendi rakendamine
Kliendiprotokoll on veidi keerulisem, sest see peab ise oma olekut haldama: millise sõnumi saata ja millal peab oma tööd „valmisks“ looma. Levinud muster on kasutada asyncio.Future
või asyncio.Event
, et anda signaal lõpetamisest tagasi peamisele korutiinile, mis käivitas kliendi.
import asyncio
class EchoClientProtocol(asyncio.Protocol):
def __init__(self, message, on_con_lost):
self.message = message
self.on_con_lost = on_con_lost
self.transport = None
def connection_made(self, transport):
self.transport = transport
print(f"Sending: {self.message}")
self.transport.write(self.message.encode())
def data_received(self, data):
print(f"Received echo: {data.decode().strip()}")
def connection_lost(self, exc):
print("The server closed the connection")
# Signal that the connection is lost and the task is complete.
self.on_con_lost.set_result(True)
def eof_received(self):
# This can be called if the server sends an EOF before closing.
print("Received EOF from server.")
async def main_client():
loop = asyncio.get_running_loop()
# The on_con_lost future is used to signal the completion of the client's work.
on_con_lost = loop.create_future()
message = "Hello World!"
host = '127.0.0.1'
port = 8888
# `create_connection` establishes the connection and links the protocol.
try:
transport, protocol = await loop.create_connection(
lambda: EchoClientProtocol(message, on_con_lost),
host,
port)
except ConnectionRefusedError:
print("Connection refused. Is the server running?")
return
# Wait until the protocol signals that the connection is lost.
try:
await on_con_lost
finally:
# Gracefully close the transport.
transport.close()
if __name__ == "__main__":
# To run the client:
# First, start the server in one terminal.
# Then, run this script in another terminal.
asyncio.run(main_client())
Siin on loop.create_connection()
kliendipoolne vaste create_server
'ile. See üritab ühendust luua antud aadressiga. Kui see õnnestub, genereerib see meie EchoClientProtocol
'i ja kutsub välja selle connection_made
meetodi. on_con_lost
Future'i kasutamine on kriitiline muster. main_client
korutiin await
s see tulevik, peatades tõhusalt omaenda täitmise, kuni protokoll annab märku, et selle töö on tehtud, kutsudes on_con_lost.set_result(True)
sisse connection_lost
.
Täiustatud mõisted ja tegeliku maailma stsenaariumid
Kaja näide hõlmab põhitõdesid, kuid tegelikud protokollid on harva nii lihtsad. Uurime mõningaid täpsemaid teemasid, millega te paratamatult kokku puutute.
Sõnumite raamistamise ja puhverdamise käsitlemine
Kõige olulisem mõiste, mida pärast põhitõdesid haarata, on see, et TCP on baitide voog. Puuduvad sisemised „sõnumi“ piirid. Kui klient saadab „Tere“ ja seejärel „Maailm“, võidakse teie serveri data_received
välja kutsuda üks kord väärtusega b'HelloWorld'
, kaks korda väärtustega b'Hello'
ja b'World'
või isegi mitu korda osaliste andmetega.
Teie protokoll vastutab „raamistamise“ eest – nende baitivoogude kokkupanemise eest mõttekateks sõnumiteks. Levinud strateegia on kasutada eraldajat, näiteks reavahemärki (
).
Siin on muudetud protokoll, mis puhverdab andmeid kuni reavahemärgi leidmiseni, töödeldes ühte rida korraga.
class LineBasedProtocol(asyncio.Protocol):
def __init__(self):
self._buffer = b''
self.transport = None
def connection_made(self, transport):
self.transport = transport
print("Connection established.")
def data_received(self, data):
# Append new data to the internal buffer
self._buffer += data
# Process as many complete lines as we have in the buffer
while b'\n' in self._buffer:
line, self._buffer = self._buffer.split(b'\n', 1)
self.process_line(line.decode().strip())
def process_line(self, line):
# This is where your application logic for a single message goes
print(f"Processing complete message: {line}")
response = f"Processed: {line}\n"
self.transport.write(response.encode())
def connection_lost(self, exc):
print("Connection lost.")
Voolu reguleerimise (vastusrõhu) haldamine
Mis juhtub, kui teie rakendus kirjutab andmeid transpordile kiiremini, kui võrk või kaugpaaris seda suudavad käsitseda? Andmed kuhjuvad transpordi sisesesse puhvrisse. Kui see jätkub kontrollimatult, võib puhver määramata ajaks kasvada, tarbides kogu saadaoleva mälu. See probleem on tuntud kui „vastusrõhu“ puudumine.
Asyncio pakub sellega tegelemise mehhanismi. Transport jälgib oma puhvri suurust. Kui puhver kasvab üle teatud kõrge veepiiri, kutsub sündmuste tsükkel välja teie protokolli pause_writing()
meetodi. See on signaal teie rakendusele, et lõpetada andmete saatmine. Kui puhver on tühjendatud alla madala veepiiri, kutsub tsükkel välja resume_writing()
, mis annab märku, et on ohutu andmeid uuesti saata.
class FlowControlledProtocol(asyncio.Protocol):
def __init__(self):
self._paused = False
self._data_source = some_data_generator() # Imagine a source of data
self.transport = None
def connection_made(self, transport):
self.transport = transport
self.resume_writing() # Start the writing process
def pause_writing(self):
# The transport buffer is full.
print("Pausing writing.")
self._paused = True
def resume_writing(self):
# The transport buffer has drained.
print("Resuming writing.")
self._paused = False
self._write_more_data()
def _write_more_data(self):
# This is our application's write loop.
while not self._paused:
try:
data = next(self._data_source)
self.transport.write(data)
except StopIteration:
self.transport.close()
break # No more data to send
# Check buffer size to see if we should pause immediately
if self.transport.get_write_buffer_size() > 0:
self.pause_writing()
Lisaks TCP-le: muud transpordid
Kuigi TCP on kõige levinum kasutusjuht, ei piirdu Transport/Protokolli muster sellega. Asyncio pakub abstraktsioone muud tüüpi suhtluseks:
- UDP: Ühenduseta suhtluseks kasutate
loop.create_datagram_endpoint()
. See annab teileDatagramTransport
ja te rakendateasyncio.DatagramProtocol
'i meetoditega nagudatagram_received(data, addr)
jaerror_received(exc)
. - SSL/TLS: Krüpteerimise lisamine on uskumatult lihtne. Edastate
ssl.SSLContext
objektiloop.create_server()
'ile võiloop.create_connection()
'ile. Asyncio tegeleb TLS-i käepigistusega automaatselt ja saate turvalise transpordi. Teie protokolli kood ei pea üldse muutuma. - Alamprotsessid: Suhtlemiseks alamprotsessidega nende standardsete I/O torude kaudu saab kasutada
loop.subprocess_exec()
jaloop.subprocess_shell()
koosasyncio.SubprocessProtocol
'iga. See võimaldab teil hallata alamprotsesse täiesti asünkroonselt, mitteblokeerival viisil.
Strateegiline otsus: millal kasutada transporte vs vooge
Kahe võimsa API abil on peamine arhitektuurne otsus valida õige töö jaoks. Siin on juhend, mis aitab teil otsustada.
Valige voogud (StreamReader
/StreamWriter
), kui...
- Teie protokoll on lihtne ja põhineb päringu-vastusel. Kui loogika on „lugege päringut, töödelge seda, kirjutage vastus“, on vood täiuslikud.
- Loote klienti tuntud rea- või fikseeritud pikkusega sõnumiprotokolli jaoks. Näiteks suhtlemine Redis serveriga või lihtsa FTP serveriga.
- Seate prioriteediks koodi loetavuse ja lineaarse, käskiva stiili.
async/await
süntaks voogudega on sageli lihtsam mõista asünkroonse programmeerimisega uutel arendajatel. - Kiire prototüüpimine on võtmetähtsusega. Saate voogudega lihtsa kliendi või serveri tööle panna vaid mõne koodireaga.
Valige transpordid ja protokollid, kui...
- Rakendate keerukat või kohandatud võrguprotokolli nullist. See on peamine kasutusjuhtum. Mõelge protokollidele mängude, finantsandmete voogude, IoT-seadmete või peer-to-peer rakenduste jaoks.
- Teie protokoll on väga sündmustepõhine ja mitte puhtalt päringu-vastus. Kui server võib igal ajal kliendile soovimatuid sõnumeid saata, on protokollide tagasihelistamisel põhinev olemus loomulikum.
- Vajate maksimaalset jõudlust ja minimaalseid kulusid. Protokollid annavad teile otsesema tee sündmuste tsüklisse, mööda minnes mõnest Vood API-ga seotud lisakulust.
- Vajate peeneteralist kontrolli ühenduse üle. See hõlmab käsitsi puhvrite haldamist, selgesõnalist voolu juhtimist (
pause/resume_writing
) ja ühenduse elutsükli üksikasjalikku käsitlemist. - Loote võrguraamistiku või teegi. Kui pakute tööriista teistele arendajatele, on protokolli/transpordi API jõuline ja paindlik olemus sageli õige alus.
Järeldus: asyncio aluse omaksvõtmine
Pythoni asyncio
teek on kihilise disaini meistriteos. Kuigi kõrgetasemeline Vood API pakub juurdepääsetavat ja produktiivset sisenemispunkti, on madala taseme Transport ja Protokolli API see, mis esindab asyncio võrgundusvõimaluste tõelist, võimsat alust. Eraldades I/O mehhanismi (Transport) rakenduse loogikast (Protokoll), pakub see jõulist, skaleeritavat ja uskumatult paindlikku mudelit keerukate võrgurakenduste ehitamiseks.
Selle madala taseme abstraktsiooni mõistmine ei ole lihtsalt akadeemiline harjutus; see on praktiline oskus, mis annab teile võimaluse liikuda lihtsatest klientidest ja serveritest kaugemale. See annab teile enesekindluse igasuguse võrguprotokolli lahendamiseks, kontrolli jõudluse optimeerimiseks surve all ja võime ehitada uue põlvkonna suure jõudlusega asünkroonseid teenuseid Pythonis. Järgmine kord, kui seisate silmitsi keerulise võrgundusprobleemiga, pidage meeles võimu, mis peitub just pinna all, ja ärge kõhelge sirutades elegantse transpordi ja protokollide duo poole.